package com.whz.governance.http.client;

import com.alibaba.cloud.nacos.NacosConfigProperties;
import com.alibaba.cloud.sentinel.datasource.RuleType;
import com.alibaba.cloud.sentinel.datasource.converter.JsonConverter;
import com.alibaba.csp.sentinel.adapter.gateway.common.api.ApiDefinition;
import com.alibaba.csp.sentinel.adapter.gateway.common.api.ApiPathPredicateItem;
import com.alibaba.csp.sentinel.adapter.gateway.common.api.ApiPredicateGroupItem;
import com.alibaba.csp.sentinel.adapter.gateway.common.api.ApiPredicateItem;
import com.alibaba.csp.sentinel.adapter.gateway.common.api.GatewayApiDefinitionManager;
import com.alibaba.csp.sentinel.adapter.gateway.common.rule.GatewayFlowRule;
import com.alibaba.csp.sentinel.adapter.gateway.common.rule.GatewayRuleManager;
import com.alibaba.csp.sentinel.datasource.nacos.NacosDataSource;
import com.alibaba.csp.sentinel.slots.block.authority.AuthorityRule;
import com.alibaba.csp.sentinel.slots.block.authority.AuthorityRuleManager;
import com.alibaba.csp.sentinel.slots.block.degrade.DegradeRule;
import com.alibaba.csp.sentinel.slots.block.degrade.DegradeRuleManager;
import com.alibaba.csp.sentinel.slots.block.flow.FlowRule;
import com.alibaba.csp.sentinel.slots.block.flow.FlowRuleManager;
import com.alibaba.csp.sentinel.slots.block.flow.param.ParamFlowRule;
import com.alibaba.csp.sentinel.slots.block.flow.param.ParamFlowRuleManager;
import com.alibaba.csp.sentinel.slots.system.SystemRule;
import com.alibaba.csp.sentinel.slots.system.SystemRuleManager;
import com.alibaba.csp.sentinel.transport.config.TransportConfig;
import com.alibaba.nacos.api.PropertyKeyConst;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.Version;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.deser.std.StdDeserializer;
import com.fasterxml.jackson.databind.module.SimpleModule;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.whz.generic.json.JacksonUtils;
import com.whz.governance.http.feign.FeignFallBackConfig;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.cloud.commons.util.InetUtils;
import org.springframework.cloud.commons.util.InetUtilsProperties;
import org.springframework.cloud.context.config.annotation.RefreshScope;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import javax.annotation.PostConstruct;
import javax.annotation.Resource;
import java.io.IOException;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.Set;

import static com.whz.governance.http.client.CommonSentinelConstants.TONGYU_SENTINEL_OPEN_AUTHORITY_ENABLE;
import static com.whz.governance.http.client.CommonSentinelConstants.TONGYU_SENTINEL_OPEN_DEGRADE_ENABLE;
import static com.whz.governance.http.client.CommonSentinelConstants.TONGYU_SENTINEL_OPEN_FLOW_ENABLE;
import static com.whz.governance.http.client.CommonSentinelConstants.TONGYU_SENTINEL_OPEN_GATEWAY_API;
import static com.whz.governance.http.client.CommonSentinelConstants.TONGYU_SENTINEL_OPEN_GATEWAY_FLOW;
import static com.whz.governance.http.client.CommonSentinelConstants.TONGYU_SENTINEL_OPEN_PARAM_ENABLE;
import static com.whz.governance.http.client.CommonSentinelConstants.TONGYU_SENTINEL_OPEN_SYSTEM_ENABLE;


/**
 * 参考: <a href="https://github.com/alibaba/Sentinel/wiki/%E5%9C%A8%E7%94%9F%E4%BA%A7%E7%8E%AF%E5%A2%83%E4%B8%AD%E4%BD%BF%E7%94%A8-Sentinel">...</a>
 *
 * @author whz
 */
@Slf4j
@RefreshScope
@Configuration
public class SentinelRulesSyncConfiguration {

    static {
        try (InetUtils inetUtils = new InetUtils(new InetUtilsProperties())) {
            // 解决部署到容器的时候,获取本地IP
            String ipAddress = inetUtils.findFirstNonLoopbackHostInfo().getIpAddress();
            log.info("Sentinel -> csp.sentinel.heartbeat.client.ip = {}", ipAddress);
            // 不要使用SentinelConfig.setConfig ,会导致SentinelConfigLoader提前加载
            System.setProperty(TransportConfig.HEARTBEAT_CLIENT_IP, ipAddress);
        } catch (Exception ignore) {
        }
    }

    @Value("${spring.application.name:none}")
    private String appName;

    @Value("${" + TONGYU_SENTINEL_OPEN_FLOW_ENABLE + ":true}")
    private boolean flowOpen;

    @Value("${" + TONGYU_SENTINEL_OPEN_DEGRADE_ENABLE + ":true}")
    private boolean degradeOpen;

    @Value("${" + TONGYU_SENTINEL_OPEN_AUTHORITY_ENABLE + ":true}")
    private boolean authorityOpen;

    @Value("${" + TONGYU_SENTINEL_OPEN_PARAM_ENABLE + ":true}")
    private boolean paramOpen;

    @Value("${" + TONGYU_SENTINEL_OPEN_SYSTEM_ENABLE + ":true}")
    private boolean systemOpen;

    @Value("${" + TONGYU_SENTINEL_OPEN_GATEWAY_FLOW + ":false}")
    private boolean gatewayFlowOpen;

    @Value("${" + TONGYU_SENTINEL_OPEN_GATEWAY_API + ":false}")
    private boolean gatewayApiOpen;

    @Resource
    private NacosConfigProperties nacosConfigProperties;

    @PostConstruct
    public void loadRuleManager() {

        // 流量
        if (flowOpen) {
            loadFlowRules();
        }
        // 服务降级
        if (degradeOpen) {
            loadDegradeRules();
        }
        // 来源限流
        if (authorityOpen) {
            loadAuthorityRules();
        }
        // 热点参数
        if (paramOpen) {
            loadParamsRules();
        }

        if (systemOpen) {
            loadSystemRules();
        }

        // gateway routeId 限流
        if (gatewayFlowOpen) {
            loadGatewayFlowRules();
        }
        // gateway 自定义api
        if (gatewayApiOpen) {
            loadGatewayApiRules();
        }
    }

    /**
     * feign.sentinel.enabled=true 启动feign的fallback
     */
    @Bean
    public FeignFallBackConfig createFeignFallBackConfig() {
        return new FeignFallBackConfig();
    }


    /**
     * dataId = sentinel-rules-flow
     */
    private void loadFlowRules() {
        NacosDataSource<List<FlowRule>> flowRuleDataSource = new NacosDataSource<List<FlowRule>>(
                getServerAddress(),
                getGroupId(),
                getDataId(RuleType.FLOW),
                new JsonConverter<>(JacksonUtils.createDefaultObjectMapper(), FlowRule.class)
        );
        FlowRuleManager.register2Property(flowRuleDataSource.getProperty());
    }

    /**
     * dataId = sentinel-rules-degrade
     */
    private void loadDegradeRules() {
        NacosDataSource<List<DegradeRule>> dataSource = new NacosDataSource<>(
                getServerAddress(),
                getGroupId(),
                getDataId(RuleType.DEGRADE),
                new JsonConverter<DegradeRule>(JacksonUtils.createDefaultObjectMapper(), DegradeRule.class));
        DegradeRuleManager.register2Property(dataSource.getProperty());
    }

    /**
     * dataId = sentinel-rules-authority
     */
    private void loadAuthorityRules() {
        NacosDataSource<List<AuthorityRule>> flowRuleDataSource = new NacosDataSource<>(
                getServerAddress(),
                getGroupId(),
                getDataId(RuleType.AUTHORITY),
                new JsonConverter<AuthorityRule>(JacksonUtils.createDefaultObjectMapper(), AuthorityRule.class)
        );
        AuthorityRuleManager.register2Property(flowRuleDataSource.getProperty());
    }

    private void loadParamsRules() {
        NacosDataSource<List<ParamFlowRule>> flowRuleDataSource = new NacosDataSource<>(
                getServerAddress(),
                getGroupId(),
                getDataId(RuleType.PARAM_FLOW),
                new JsonConverter<ParamFlowRule>(JacksonUtils.createDefaultObjectMapper(), ParamFlowRule.class)
        );
        ParamFlowRuleManager.register2Property(flowRuleDataSource.getProperty());
    }

    private void loadSystemRules() {
        NacosDataSource<List<SystemRule>> flowRuleDataSource = new NacosDataSource<>(
                getServerAddress(),
                getGroupId(),
                getDataId(RuleType.SYSTEM),
                new JsonConverter<SystemRule>(JacksonUtils.createDefaultObjectMapper(), SystemRule.class)
        );
        SystemRuleManager.register2Property(flowRuleDataSource.getProperty());
    }

    private void loadGatewayFlowRules() {
        NacosDataSource<Set<GatewayFlowRule>> flowRuleDataSource = new NacosDataSource<>(
                getServerAddress(),
                getGroupId(),
                getDataId(RuleType.GW_FLOW),
                new JsonConverter<GatewayFlowRule>(JacksonUtils.createDefaultObjectMapper(), GatewayFlowRule.class)
        );
        GatewayRuleManager.register2Property(flowRuleDataSource.getProperty());
    }

    private void loadGatewayApiRules() {
        NacosDataSource<Set<ApiDefinition>> flowRuleDataSource = new NacosDataSource<>(
                getServerAddress(),
                getGroupId(),
                getDataId(RuleType.GW_API_GROUP),
                new JsonConverter<ApiDefinition>(buildApiDefinitionObjectMapper(), ApiDefinition.class)
        );
        GatewayApiDefinitionManager.register2Property(flowRuleDataSource.getProperty());
    }


    /**
     * dataId={appName}-sentinel-{ruleType}
     */
    private String getDataId(RuleType type) {
        return appName + "-sentinel-" + type.getName();
    }

    private Properties getServerAddress() {
        Properties properties = new Properties();
        properties.setProperty(PropertyKeyConst.SERVER_ADDR, nacosConfigProperties.getServerAddr());
        properties.setProperty(PropertyKeyConst.NAMESPACE, nacosConfigProperties.getNamespace());
        return properties;
    }

    private String getGroupId() {
        return nacosConfigProperties.getGroup();
    }


    private static ObjectMapper buildApiDefinitionObjectMapper() {
        ObjectMapper objectMapper = JacksonUtils.createDefaultObjectMapper();
        ApiPredicateItemDeserializer deserializer = new ApiPredicateItemDeserializer();
        deserializer.registerApiPredicateItem("pattern", ApiPathPredicateItem.class);
        deserializer.registerApiPredicateItem("items", ApiPredicateGroupItem.class);
        SimpleModule module = new SimpleModule(
                "PolymorphicApiPredicateItemDeserializerModule",
                new Version(1, 0, 0, null, null, null));
        module.addDeserializer(ApiPredicateItem.class, deserializer);
        objectMapper.registerModule(module);
        return objectMapper;
    }


    /**
     * refer: SentinelGatewayAutoConfiguration
     */
    static class ApiPredicateItemDeserializer extends StdDeserializer<ApiPredicateItem> {

        private final Map<String, Class<? extends ApiPredicateItem>> registry =
                new HashMap<String, Class<? extends ApiPredicateItem>>();

        ApiPredicateItemDeserializer() {
            super(ApiPredicateItem.class);
        }

        void registerApiPredicateItem(String uniqueAttribute,
                                      Class<? extends ApiPredicateItem> apiPredicateItemClass) {
            registry.put(uniqueAttribute, apiPredicateItemClass);
        }

        @Override
        public ApiPredicateItem deserialize(JsonParser jp,
                                            DeserializationContext ctxt) throws IOException {
            ObjectMapper mapper = (ObjectMapper) jp.getCodec();
            ObjectNode root = mapper.readTree(jp);
            Class<? extends ApiPredicateItem> apiPredicateItemClass = null;
            Iterator<Map.Entry<String, JsonNode>> elementsIterator = root.fields();
            while (elementsIterator.hasNext()) {
                Map.Entry<String, JsonNode> element = elementsIterator.next();
                String name = element.getKey();
                if (registry.containsKey(name)) {
                    apiPredicateItemClass = registry.get(name);
                    break;
                }
            }
            if (apiPredicateItemClass == null) {
                return null;
            }
            return mapper.readValue(root.toString(), apiPredicateItemClass);
        }
    }
}